package Share;

import java.io.*;
import java.security.*;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.concurrent.atomic.AtomicBoolean;

public class TransmitionBox {
	
	private ConnectionDelegate listener;
	private Certificate certificate;
	private Session session;

	private KeyPair key;
	
	public Certificate getCertificate()
	{
		return certificate;
	}
	
	public Boolean hasCertificate()
	{
		return certificate != null;
	}
	
	public TransmitStatus requestCertificate(String name, String server, int port)
	{
		Socket socket = null;
		DataOutputStream out = null;
		DataInputStream in = null;
		try
		{
			try
			{
				socket = new Socket(server, port);
			}
			catch(Exception e)
			{
				return TransmitStatus.hostNotFound;
			}
			out = new DataOutputStream(socket.getOutputStream());
			in = new DataInputStream(socket.getInputStream());
			
			KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
			keyGen.initialize(2048);
			key = keyGen.genKeyPair();
			
			
			byte[] pubkey = key.getPublic().getEncoded();
			
			Util.write(out,name);
			Util.write(out,pubkey);
			
			TransmitStatus status = TransmitStatus.values()[in.read()];
			if(status != TransmitStatus.success)
				return status;
			
			certificate = new Certificate(in);
			if(!Util.verifyCertificate(certificate))
			{
				certificate = null;
				return TransmitStatus.invalidCertificate;
			}
			
			return TransmitStatus.success;
		}
		catch(Exception error)
		{
			certificate = null;
			return TransmitStatus.error;
		}
		finally
		{
			try
			{
				in.close();
				out.close();
				socket.close();
			}
			catch(Exception e)
			{
				
			}
		}			
	}
	
	public String getName()
	{
		if(certificate == null)
			return "#none#";
		else
			return certificate.owner;
	}
	
	public void stop()
	{
		if(listener != null)
			listener.stop();
	}
	
	public TransmitStatus startSession(String serverName, int portNumber)
	{
		if(session != null)
		{
			try {
				session.close();
			} catch (IOException e) {
			}
			session = null;
		}
		try
		{
			session = new Session(serverName, portNumber, certificate, key.getPrivate());
			if(!session.getRemoteCertificate().owner.equals("server"))
			{
				endSession();
				return TransmitStatus.invalidName;
			}
		}
		catch(Exception e)
		{
			return TransmitStatus.error;
		}
		return TransmitStatus.success;
	}
	
	public void endSession()
	{
		try
		{
			if(session != null)
				session.close();
			session = null;
		}
		catch(Exception e)
		{
		}
	}
	
	private class ConnectionDelegate implements Runnable
	{
		private WaitRequestHandler handler;
		private int port;
		private ConnectionWaiter serverSocket;
		private AtomicBoolean stopped = new AtomicBoolean(false);
		private PrivateKey key;
		private Certificate certificate;
		
		public ConnectionDelegate(int port, WaitRequestHandler handler, Certificate certificate, PrivateKey key)
		{
			this.handler = handler;
			this.port = port;
			this.certificate = certificate;
			this.key = key;
		}
		
		public void stop()
		{
			try
			{
				stopped.set(true);
				if(serverSocket != null)
					serverSocket.close();
			}
			catch (IOException e)
			{

			}
		}
		
		@Override
		public void run() {
			try {
				serverSocket = new ConnectionWaiter(port, certificate, key);
				while(!stopped.get())
				{
					Session session = null;
					try {
						session = serverSocket.accept();
					} catch (Exception e) {
						if(stopped.get())
							return;
						else
							handler.handlerError(e);
							
					}
					if(session != null)
					{
						new Thread(new ConnectionHandle(handler, session)).start();
					}
				}
			}
			catch (Exception e) {
				handler.handlerError(e);
			}
		}	
	}
	
	public void stopConnectionWait()
	{
		listener.stop();
		listener = null;
	}
	
	public boolean waitConnection(int port, WaitRequestHandler handler) throws IOException
	{
		
		if(listener != null)
		{
			return false;
		}
		if(certificate == null)
			return false;
		listener = new ConnectionDelegate(port, handler, certificate, key.getPrivate());
		if(handler.blocking)
			listener.run();
		else
			new Thread(listener).start();
		return true;
	}
	
	private class ConnectionHandle implements Runnable
	{
		private WaitRequestHandler handle;
		private Session session;
		
		public ConnectionHandle(WaitRequestHandler handle, Session socket)
		{
			this.handle = handle;
			this.session = socket;
		}
		
		
		public void run()
		{
			SecureDataInputStream in = null;
			SecureDataOutputStream out = null;
			try
			{
				in = session.getInputStream();
				out = session.getOutputStream();
				while(!session.isClosed())
				{
					Token token = new Token(in);
					if(!session.getRemoteCertificate().owner.equals(token.signer))
						out.write(TransmitStatus.wrongToken.ordinal());
					else if (!session.getLocalCertificate().owner.equals(token.deleg))
						out.write(TransmitStatus.wrongToken.ordinal());
					else if (!Util.validateToken(token))
						out.write(TransmitStatus.invalidToken.ordinal());
					else
					{
						if(!handle.connectionRecieved(token, out, in))
						{
							if(session != null && !session.isClosed())
								session.close();
						}
					}
				}
			}
			catch(Exception error)
			{
				System.out.println("The session was lost.");
				if(session != null && !session.isClosed())
					try {
						session.close();
					} catch (IOException e) {

					}
			}
		}
		
	}
	
	public TransmitStatus getFile(Token token, File file)
	{
		if(session == null)
			return TransmitStatus.noSession;
		
		OutputStream writer = null;
		try
		{
			File temp = File.createTempFile(file.getName(), "temp");
			try
			{	
				if((!file.exists() || !file.canWrite()) && temp.createNewFile())
					return TransmitStatus.clientWriteError;
				writer = new FileOutputStream(temp);
			}
			catch(FileNotFoundException error)
			{
				return TransmitStatus.clientWriteError;
			}
			
			SecureDataOutputStream out = session.getOutputStream();
			SecureDataInputStream in = session.getInputStream();
		
			//Start Transaction;
			token.serialize(out);
			
			//Check Accepted
			TransmitStatus status = TransmitStatus.deserialize(in.read());
			if(status != TransmitStatus.success)
				return status;

			try
			{
				//Start Hash
				in.startHash();
				//Read File Size
				long remaining = in.readLong();
				//Read File
				byte[] buffer = new byte[256];
				while(remaining > 0)
				{
					int writeCount = (int) (remaining % (buffer.length + 1));
					in.read(buffer, 0, writeCount);
					writer.write(buffer, 0, writeCount);
					remaining -= writeCount;
				}
				writer.close();
				
				//Read Hash
				boolean goodHash = in.checkHash();
					
				//Read Final Report
				status = TransmitStatus.deserialize(in.read());
				
				if(!goodHash)
					return TransmitStatus.badHash;
				
				if((!file.exists() || file.delete()) && temp.renameTo(file))
					return status;
				
				return TransmitStatus.clientWriteError;
				
			}
			catch(IOException error)
			{
				try
				{
					temp.delete();
				}
				catch(Exception e)
				{
				}
				return TransmitStatus.clientFileReceiveError;
				
			}
			finally
			{
				if(writer != null)
					writer.close();
			}
		}
		catch(IOException error)
		{
			return TransmitStatus.error;
		}
		finally
		{
			try {
				if(writer != null)
					writer.close();
			} catch (IOException e) {
			}
		}
	}
	
	public TransmitStatus putFile(Token token, File file)
	{
		if(session == null)
			return TransmitStatus.noSession;
		
		FileInputStream reader;
		try
		{
			try
			{
				if(!file.canRead())
					return TransmitStatus.clientFileSendError;
				reader = new FileInputStream(file);
			}
			catch(FileNotFoundException error)
			{
				return TransmitStatus.clientFileNotFound;
			}
			
			
			SecureDataOutputStream out = session.getOutputStream();
			SecureDataInputStream in = session.getInputStream();
			
			if(!session.getRemoteCertificate().owner.equals("server"))
			{
				out.write(TransmitStatus.wrongToken.ordinal());
				return TransmitStatus.wrongToken;
			}
			//Transaction Starts
			token.serialize(out);
			
			//Verify Transaction Accepted
			TransmitStatus status = TransmitStatus.deserialize(in.read());
			if(status != TransmitStatus.success)
				return status;
			
			try
			{
				//Start hash
				out.startHash();
				//Write File Size
				out.writeLong(file.length());
				//Write File
				byte[] buffer = new byte[256];
				int count = reader.read(buffer);
				while(count >= 0)
				{
					out.write(buffer, 0, count);
					count = reader.read(buffer);
				}
				
				//Write hash
				out.writeHash();
			}
			catch(IOException error)
			{
				return TransmitStatus.clientFileSendError;
			}
			finally
			{
				if(reader != null)
					reader.close();
			}
			
			//Read Final Report
			status = TransmitStatus.deserialize(in.read());

			return status;
		}
		catch(UnknownHostException error)
		{
			return TransmitStatus.hostNotFound;
		}
		catch(IOException error)
		{
			return TransmitStatus.error;
		}
	}
	
	public TransmitStatus SendToken(Token token, String destination, int port)
	{
		Session session = null;
		SecureDataOutputStream out = null;
		SecureDataInputStream in = null;
		try
		{
			session = new Session(destination, port, certificate, key.getPrivate());
			out = session.getOutputStream();
			in = session.getInputStream();
			
			token.serialize(out);
			
			return TransmitStatus.values()[in.read()];
		}
		catch(UnknownHostException error)
		{
			return TransmitStatus.hostNotFound;
		}
		catch(Exception error)
		{
			return TransmitStatus.error;
		}
		finally
		{
			try
			{
				if(session != null)
					session.close();
			}
			catch(IOException error){}
		}
	}

	public PrivateKey getPrivateKey() {
		return key.getPrivate();
	}
}
